<?php

namespace common\service;

use common\exception\LogicException;
use common\model\MemberModel;
use common\model\PlanModel;
use common\model\SaasEntityModel;
use common\model\SaasProjectModel;
use common\model\SaasTaskModel;
use common\model\TaskMonthWorkHourRecordModel;
use common\model\WorkHourRecordModel;
use Exception;
use support\ErrorCode;
use support\SingletonTrait;
use Throwable;

class ReportService
{
    use SingletonTrait;

    protected $canPaddle = false; // 能不能划水

    protected array $taskListQueryFields = [
        'task.id',
        'task.project_id',
        'task.entity_id',
        'task.episode_id',
        'task.step_category_id',
        'task.step_id',
        'task.man_hour',
        "concat(entity.initial_episode_code, '/', entity.showings_number, '/', entity.name, '/', task.name) task_name",
        'task.order_str',
        'entity.initial_episode_code',
        'entity.showings_number',
        'entity.module_id',
        'entity.name',
        'entity.json',
    ];

    /**
     * 生成报表
     * @param string $month 生成得月份
     * @param string $stepCategoryName 要生成得环节名称
     * @return array
     * @throws Throwable
     */
    public function generate(int $projectId, string $month, string $stepCategoryName)
    {

        $showStepCategoryList = teamones_request('teamones-saas', 'project/get_show_step_category_group', []);
        $showStepCategoryMap = array_column($showStepCategoryList['data'], null, 'name');

        if (!array_key_exists($stepCategoryName, $showStepCategoryMap)) {
            throw new LogicException('step_category_name not found', ErrorCode::SHOW_STEP_CATEGORY_NOT_FOUND);
        }
        $showStepCategory = $showStepCategoryMap[$stepCategoryName];
        $monthStartTimestamp = strtotime(date("Y-m-01 00:00:00", strtotime($month)));
        $monthEndTimestamp = strtotime("+1 month - 1 second", $monthStartTimestamp);

        // todo debug
        model(TaskMonthWorkHourRecordModel::class)->execute("delete from task_month_work_hour_record where step_category_name = '{$stepCategoryName}' and month_time = {$monthStartTimestamp} and project_id = {$projectId}");
        model(TaskMonthWorkHourRecordModel::class)->execute("delete from work_hour_record where step_category_name = '{$stepCategoryName}' and month_time = {$monthStartTimestamp} and project_id = {$projectId}");

        // 检查是否生成过
        $workHourFirst = model(WorkHourRecordModel::class)->where(['project_id' => $projectId, 'month_time' => $monthStartTimestamp, 'step_category_name' => $stepCategoryName])->find();
        if (!empty($workHourFirst)) {
            throw new LogicException('already generated', ErrorCode::WORK_HOUR_ALREADY_GENERATED);
        }
        // 查询计划
        $planList = $this->getPlanList($projectId, $monthStartTimestamp, $monthEndTimestamp, $stepCategoryName);
        if (empty($planList)) {
            throw new LogicException('current month don`t have plan', ErrorCode::CURRENT_MONTH_DONT_HAVE_PLAN);
        }
        $episodeIdList = array_column($planList, 'episode_id');
        //  查询集/环节已经消耗的工作量
        $taskRemainRecordList = $this->getTaskMonthWorkHourRecord($episodeIdList, $showStepCategory['children_ids']);
        // 查询任务
        $taskList = $this->getTaskList($episodeIdList, $showStepCategory['children_ids']);
        if (empty($taskList)) {
            throw new LogicException('plan don`t have taskList', ErrorCode::PLAN_EPISODE_DONT_HAVE_TASK_LIST);
        }
        // 计算剧集的总工作量
        $episodeManHourTotal = []; // 剧集总的标准工时
        $episodeManHourRemainActually = []; // 剧集实际剩余标准工时
        $taskAvailableList = []; // 还有标准工时的任务 可用来进行消费用户工时
        foreach ($taskList as $taskIndex => &$task) {
            if (!isset($episodeManHourTotal[$task['episode_id']])) {
                $episodeManHourTotal[$task['episode_id']] = $task['man_hour'];
            } else {
                $episodeManHourTotal[$task['episode_id']] += $task['man_hour'];
            }
            // 计算任务的实际可用工时
            if (!in_array($task['id'], $taskRemainRecordList['done'])) {
                if (!empty($taskRemainRecordList['inprogress'][$task['id']])) {
                    $task['actually_man_hour'] = $taskRemainRecordList['inprogress'][$task['id']];
                } else {
                    $task['actually_man_hour'] = $task['man_hour'];
                }
                if (!isset($episodeManHourRemainActually[$task['episode_id']])) {
                    $episodeManHourRemainActually[$task['episode_id']] = $task['actually_man_hour'];
                } else {
                    $episodeManHourRemainActually[$task['episode_id']] += $task['actually_man_hour'];
                }
                // 标记任务为可以拿来消费的任务
                $task['repair_flag'] = false;
                $task['repair_hour'] = 0;
                $taskAvailableList[] =  &$taskList[$taskIndex];
            }
        }
        $availableTaskCount = count($taskAvailableList);
        $needRepairTaskCount = floor($availableTaskCount * $this->getTaskRepairChancePercent());
        $needRepairTaskIndexList = [];
        if ($needRepairTaskCount > 0) {
            // 随机出需要返修的任务
            for ($i = 1; $i <= $needRepairTaskCount; $i++) {
                $randomRepairTaskIndex = random_int(0, $availableTaskCount - 1);
                if (in_array($randomRepairTaskIndex, $needRepairTaskIndexList)) {
                    $i--;
                } else {
                    $needRepairTaskIndexList[] = $randomRepairTaskIndex;
                }
            }

            // 增加要花费的工时
            $repairIncreaseHourPercent = $this->getTaskRepairIncreaseHourPercent();
            foreach ($needRepairTaskIndexList as $needRepairTaskIndex) {
                $taskAvailableList[$needRepairTaskIndex]['repair_flag'] = true;
                $currentTaskRepairHour = $taskAvailableList[$needRepairTaskIndex]['man_hour'] * $repairIncreaseHourPercent;
                $taskAvailableList[$needRepairTaskIndex]['repair_hour'] = $currentTaskRepairHour;
                $taskAvailableList[$needRepairTaskIndex]['actually_man_hour'] += $taskAvailableList[$needRepairTaskIndex]['repair_hour'];
                $episodeManHourRemainActually[$taskAvailableList[$needRepairTaskIndex]['episode_id']] += $currentTaskRepairHour;
            }
        }

        // 利用计划比例计算出本月要消耗的标准任务工时
        $planEpisodeManHour = [];
        $taskManHourTotal = 0; // 总任务工时
        foreach ($planList as $plan) {
            // 剧集总工时有没有
            if (empty($episodeManHourTotal[$plan['episode_id']])) {
                continue;
            }
            $currentOutsideExeHour = 0;
            // 当前剧集外包工时
            if (!empty($plan['outside_exe_percent'])) {
                $currentOutsideExeHour = $episodeManHourTotal[$plan['episode_id']] * ($plan['outside_exe_percent'] / 100);
            }
            $currentEpisodeNeedConsume = ($episodeManHourTotal[$plan['episode_id']] - $currentOutsideExeHour) * $plan['time_length_percent']; // 剧集本月需要消费工时
            // 剧集可用工时有没有
            if (empty($episodeManHourRemainActually[$plan['episode_id']])) {
                continue;
            }
            $currentEpisodeRemain = $episodeManHourRemainActually[$plan['episode_id']]; // 剧集当前还剩余工时
            // 当前需要消费的工时大于剧集可用工时 那么当前需要消费的就等于剩余工时
            if ($currentEpisodeNeedConsume > $currentEpisodeRemain) {
                $currentEpisodeNeedConsume = $currentEpisodeRemain;
            }
            $planEpisodeManHour[$plan['episode_id']] = $currentEpisodeNeedConsume;
            $taskManHourTotal += $currentEpisodeNeedConsume;
        }
        // 查询环节人员情况
        $userList = model(MemberModel::class)->where(['project_id' => $projectId, 'month_time' => $monthStartTimestamp, 'step_category_name' => $showStepCategory['name']])->select();
        $userIdList = array_column($userList, 'user_id');
        if (empty($userIdList)) {
            throw new LogicException('month member is empty', ErrorCode::MONTH_MEMBER_IS_EMPTY);
        }
        // 查询人员工时情况
        $userWorkHourSituationList = $this->getUserWorkHourList($userList, $monthStartTimestamp, $monthEndTimestamp);

        // 计算钉钉工时和任务工时的系数
        $userWorkHourTotal = array_sum(array_column($userWorkHourSituationList, 'work_hour'));
        $workHourRatio = $taskManHourTotal / $userWorkHourTotal; // 任务工时和用户工时的比例
        // $workHourRatio = $workHourRatio * 0.8;
        // 得到排序过的待消费任务列表
        $currentModule = SaasModuleService::getInstance()->byId($taskAvailableList[0]['module_id']);
        TaskListSorterService::getInstance($currentModule['code'])->sort($taskAvailableList);

        // 开始消费任务工时
        unset($task);
        $workHourInsertList = [];
        $taskMonthWorkHourRecordInsertList = [];

        $res = [
            'task_total' => $taskManHourTotal,
            'user_total' => $userWorkHourTotal,
            'ratio' => $workHourRatio,
            'plan_source' => $planEpisodeManHour,
        ];
        foreach ($taskAvailableList as $task) {
            TASK_START:
            APP_DEBUG && trace('TASK ' . $task['task_name'] . ' [' . $task['id'] . ']', 'DEBUG');
            // 本月工时都被消费完毕 结束循环
            if ($planEpisodeManHour[$task['episode_id']] <= 0) {
                APP_DEBUG && trace('EPISODE ' . $task['episode_id'] . 'hour cost done', 'DEBUG');
                continue;
            } else {
                APP_DEBUG && trace('EPISODE ' . $task['episode_id'] . ': task work hour' . $planEpisodeManHour[$task['episode_id']], 'DEBUG');
            }
            $currentDayTime = $monthStartTimestamp;
            $userWorkHour = $task['actually_man_hour'] / $workHourRatio; // 任务标准工时 转换为用户标准工时
            $taskRepairUserWorkHour = $task['repair_hour'] / $workHourRatio; // 任务返修工时
            //            $taskRepairUserWorkHour = 0; // 任务返修工时
            APP_DEBUG && trace("actually_man_hour: {$task['actually_man_hour']} userWorkHour: {$userWorkHour}", 'DEBUG');

            list($bestUserIndex, $bestUserAvailableDaysIndexList) = $this->getBestUser($userWorkHourSituationList, $userWorkHour, $currentDayTime);
            // 没有匹配到任何用户
            if (is_null($bestUserIndex)) {
                APP_DEBUG && trace('TASK ' . $task['id'] . 'can`t find suitable user', 'DEBUG');
                continue;
            }
            APP_DEBUG && trace('USER: ' . $userWorkHourSituationList[$bestUserIndex]['user_id'], 'DEBUG');
            $presetData = [
                'tenant_id' => auto_fill_tenant_id(),
                'project_id' => $task['project_id'],
                'user_id' => $userWorkHourSituationList[$bestUserIndex]['user_id'],
                'step_category_ids' => json_encode($showStepCategory['children_ids']),
                'step_category_name' => $showStepCategory['name'],
                'entity_id' => $task['entity_id'],
                'module_id' => $task['module_id'],
                'episode_id' => $task['episode_id'],
                'task_id' => $task['id'],
                'step_category_id' => $task['step_category_id'],
                'step_id' => $task['step_id'],
                'month_time' => $monthStartTimestamp,
                'created' => time(),
                'created_by' => fill_created_by(),
                'hour_mark' => 'default',
            ];
            // 先消耗制作工时
            $progressUserWorkHour = $userWorkHour - $taskRepairUserWorkHour;
            $this->doConsumeUserWorkHour($task, $bestUserAvailableDaysIndexList,
                $progressUserWorkHour, $workHourRatio, $userWorkHourSituationList[$bestUserIndex]['grade_radio'],
                $planEpisodeManHour, $workHourInsertList, $presetData);
            if ($progressUserWorkHour > 0) {
                goto TASK_START;
            }
            // 看工时是否消耗完毕 消耗完了 就消耗返修工时
            if ($progressUserWorkHour <= 0 && $taskRepairUserWorkHour > 0) {

                // todo 如何跳过一点时间
                $hourSumFromEnd = 0;
                $daysFromEnd = 0;
                $bestUserAvailableDaysKeys = array_keys($bestUserAvailableDaysIndexList);
                $daysTotal = count($bestUserAvailableDaysIndexList);
                for ($i = $daysTotal - 1; $i >= 0; --$i) {
                    if ($hourSumFromEnd > $taskRepairUserWorkHour) {
                        break;
                    }
                    $hourSumFromEnd += $bestUserAvailableDaysIndexList[$bestUserAvailableDaysKeys[$i]];
                    $daysFromEnd += 1;
                }
                $skipDays = random_int(0, $daysTotal - $daysFromEnd);
                $bestUserAvailableDaysIndexList = array_slice($bestUserAvailableDaysIndexList, $skipDays, $daysTotal - $skipDays, true);
                $presetData['hour_mark'] = 'repair';
                $this->doConsumeUserWorkHour($task, $bestUserAvailableDaysIndexList,
                    $taskRepairUserWorkHour, $workHourRatio, $userWorkHourSituationList[$bestUserIndex]['grade_radio'],
                    $planEpisodeManHour, $workHourInsertList, $presetData);
            }

            $userWorkHour = $progressUserWorkHour + $taskRepairUserWorkHour;
            $taskMonthWorkHourRecordInsertList[] = [
                'tenant_id' => auto_fill_tenant_id(),
                'project_id' => $task['project_id'],
                'user_id' => $userWorkHourSituationList[$bestUserIndex]['user_id'],
                'step_category_ids' => json_encode($showStepCategory['children_ids']),
                'episode_id' => $task['episode_id'],
                'step_category_name' => $showStepCategory['name'],
                'entity_id' => $task['entity_id'],
                'module_id' => $task['module_id'],
                'task_id' => $task['id'],
                'step_category_id' => $task['step_category_id'],
                'step_id' => $task['step_id'],
                'month_time' => $monthStartTimestamp,
                'remain' => $userWorkHour * $workHourRatio,
                'created' => time(),
                'created_by' => fill_created_by(),
            ];
        }

        $model = model(WorkHourRecordModel::class);
        $model->startTrans();
        try {

            if (!empty($workHourInsertList)) {
                foreach (array_chunk($workHourInsertList, 100) as $batchInsert) {
                    model(WorkHourRecordModel::class)->addAll($batchInsert);
                }
            }
            if (!empty($taskMonthWorkHourRecordInsertList)) {
                foreach (array_chunk($taskMonthWorkHourRecordInsertList, 100) as $batchInsert) {
                    model(TaskMonthWorkHourRecordModel::class)->addAll($batchInsert);
                }
            }
            $model->commit();
        } catch (Throwable $e) {
            $model->rollback();
            throw $e;
        }
        $res['user_situation'] = $userWorkHourSituationList;
        $res['episode_situation'] = $planEpisodeManHour;
        return $res;
    }

    /**
     * 查询出本月的计划
     * @param int $projectId
     * @param int $start
     * @param int $end
     * @param string $stepCategoryName
     * @return array|false|mixed|string
     */
    private function getPlanList(int $projectId, int $start, int $end, string $stepCategoryName)
    {
        // 与本月时间线有交叉的计划
        $planList = model(PlanModel::class)
            ->where([
                'start_time' => ['LT', $end], 'end_time' => ['GT', $start],
                'step_category_name' => $stepCategoryName,
                'project_id' => $projectId,
            ])
            ->field('project_id,episode_id,start_time,end_time,outside_exe_percent')
            ->select();
        foreach ($planList as &$plan) {
            if ($plan['end_time'] - $plan['start_time'] === 0) {
                $plan['time_length_percent'] = 1;
                continue;
            }
            // 计算实际的开始时间和结束时间
            $plan['actually_start'] = $plan['actually_end'] = 0;
            if ($plan['start_time'] > $start) {
                $plan['actually_start'] = $plan['start_time'];
            } else {
                $plan['actually_start'] = $start;
            }
            if ($plan['end_time'] > $end) {
                $plan['actually_end'] = $end;
            } else {
                $plan['actually_end'] = $plan['end_time'];
            }
            // 计算在计划内的占据长度
            $plan['time_length_percent'] = ($plan['actually_end'] - $plan['actually_start']) / ($plan['end_time'] - $plan['start_time']);
        }
        return $planList;
    }

    /**
     * 查询任务列表
     * @param array $episodeIdList
     * @param array $stepCategoryIdList
     * @return array|false|mixed|string
     */
    private function getTaskList(array $episodeIdList, array $stepCategoryIdList)
    {
        $taskFilter = [
            'task.episode_id' => ['IN', implode(',', $episodeIdList)],
            'task.step_category_id' => ['IN', implode(',', $stepCategoryIdList)],
            'task.step_type' => SaasTaskModel::STEP_TYPE_STEP,
            'task.man_hour' => ['GT', 0],
        ];
        $closedStatus = teamones_request('teamones-saas', 'status/get_default_status', ['name' => 'default_deprecate_status'])['data'];

        if (!empty($closedStatus)) {
            $taskFilter["task.json->>'$.task_status'"] = [
                'NEQ', $closedStatus['id'],
            ];
        }
        $taskList = model(SaasTaskModel::class)
            ->join('entity on entity.id = task.entity_id')
            ->where($taskFilter)
            ->field($this->taskListQueryFields)
            ->select();

        foreach ($taskList as &$task) {
            $entityJson = json_decode($task['json'], true);
            $currentEntityModule = SaasModuleService::getInstance()->byId($task['module_id']);
            foreach ($entityJson as $ek => $ev) {
                $task[str_replace_once($currentEntityModule['code'], '', $ek)] = $ev;
            }
        }
        return $taskList;
    }

    /**
     * 查询月度剩余工时任务情况
     * @param array $episodeIdList
     * @param array $stepCategoryIdList
     * @return array
     */
    private function getTaskMonthWorkHourRecord(array $episodeIdList, array $stepCategoryIdList)
    {
        // 按照时间倒序查询出(集和环节)每个任务的剩余工时
        $list = model(TaskMonthWorkHourRecordModel::class)
            ->where([
                'episode_id' => ['IN', implode(',', $episodeIdList)],
                'step_category_id' => ['IN', implode(',', $stepCategoryIdList)],
            ])
            ->field('task_id,min(remain) remain')
            ->group('task_id')
            ->select();
        $res = [
            'done' => [],
            'inprogress' => [],
        ];
        foreach ($list as $item) {
            if ($item['remain'] == 0) {
                // 工时剩余为0 就标记为已经完成
                $res['done'][] = $item['task_id'];
            } else {
                // 工时剩余大于0
                $res['inprogress'][$item['task_id']] = $item['remain'];
            }
        }
        return $res;
    }

    /**
     * 查询用户工时列表
     * @param array $userList
     * @param int $monthStartTimestamp
     * @param int $monthEndTimestamp
     * @param string $type
     * @return array
     * @throws Exception
     */
    private function getUserWorkHourList(array $userList, int $monthStartTimestamp, int $monthEndTimestamp, string $type = 'user_filled')
    {
        $userGradeWorkRadio = $this->getUserGradeWorkHourRadio();
        $userIdList = array_column($userList, 'user_id');
        $uGradeMap = array_column($userList, 'grade', 'user_id');
        $queryParam = [
            'user_id_list' => $userIdList,
            'work_date_from' => date("Y-m-d 00:00:00", $monthStartTimestamp),
            'work_date_to' => date("Y-m-d 00:00:00", $monthEndTimestamp),
        ];
        if ($type === 'user_filled') {
            $userWorkHourList = teamones_request('teamones-olap-sr', 'work_time/get_user_work_times', $queryParam)['data'];
        } else {
            $userWorkHourList = teamones_request('teamones-work-hour', 'dingtalk/get_user_work_times', $queryParam)['data'];
        }
        // 把分钟转换成小时
        $maxPaddlePercent = $this->getMaxPaddlePercent();
        foreach ($userWorkHourList as &$userWorkHourItem) {
            $uGrade = $uGradeMap[$userWorkHourItem['user_id']];
            $userWorkHourItem['grade_radio'] = $userGradeWorkRadio[$uGrade] ?? 1;
            $userWorkHourItem['source_work_hour'] = $userWorkHourItem['work_hour'];
            $userWorkHourItem['work_hour'] = 0;
            $userWorkHourItem['paddle_hour'] = 0;
            foreach ($userWorkHourItem['work_every_day_min'] as &$workEveryDayMin) {
                $workEveryDayHour = $workEveryDayMin / 60;
                $paddleHour = random_int(0, $maxPaddlePercent) / 100;
                if ($this->canPaddle) {
                    // 计算出今日划水工时 小于今日工时 那么扣掉划水工时
                    if ($paddleHour < $workEveryDayHour) {
                        $workEveryDayHour -= $paddleHour;
                        $userWorkHourItem['paddle_hour'] += $paddleHour;
                    }
                }
                $workEveryDayMin = $workEveryDayHour * $userWorkHourItem['grade_radio'];
                $userWorkHourItem['work_hour'] += $workEveryDayMin;
            }
        }
        return array_sort_by($userWorkHourList, 'work_hour', 'desc');
    }

    /**
     * 查找最接近工时的用户
     * @param array $userWorkHourSituationList
     * @param float $userWorkHour
     * @param $after
     * @return array
     */
    private function getBestUser(array &$userWorkHourSituationList, float $userWorkHour, $after)
    {
        shuffle($userWorkHourSituationList);
        APP_DEBUG && trace('find most close work hour to ' . $userWorkHour, 'DEBUG');
        $currentIndex = null;
        $minAbs = null;
        $daysAvailable = [];
        foreach ($userWorkHourSituationList as $uwIndex => $item) {
            $itemLeastHourDays = array_filter($item['work_every_day_min'], function ($v, $k) use ($after) {
                return $k >= $after && $v > 0;
            }, ARRAY_FILTER_USE_BOTH);
            $itemLeastHour = array_sum($itemLeastHourDays);
            if ($itemLeastHour <= 0) {
                // 用户已经没有工时了
                continue;
            }
            APP_DEBUG && trace($item['user_id'] . ': ' . $itemLeastHour, 'DEBUG');
            $currentAbs = abs($itemLeastHour - $userWorkHour);
            if (is_null($minAbs)) {
                $minAbs = $currentAbs;
                $currentIndex = $uwIndex;
                $daysAvailable = array_keys($itemLeastHourDays);
            } elseif ($currentAbs < $minAbs) {
                $minAbs = $currentAbs;
                $currentIndex = $uwIndex;
                $daysAvailable = array_keys($itemLeastHourDays);
                // 当命中第二个大于可消费的工时 就跳过 避免每次都命中一个人
                if ($itemLeastHour - $userWorkHour > 0) {
                    APP_DEBUG && trace('breaking', 'DEBUG');
                    break;
                }
            }
        }
        $daysAvailableAddress = [];
        foreach ($daysAvailable as $daysAvailableIndex) {
            $daysAvailableAddress[$daysAvailableIndex] =  &$userWorkHourSituationList[$currentIndex]['work_every_day_min'][$daysAvailableIndex];
        }

        return [$currentIndex, $daysAvailableAddress];
    }

    /**
     * 工时记录列表
     * @param int $tenantId
     * @param string $bigModuleCode
     * @param array $filter
     * @param array $page
     * @param string $queryType
     * @param $entityNameSearch
     * @return array
     */
    function list(int $tenantId, string $bigModuleCode, array $filter, array $page, string $queryType, $entityNameSearch) {
        $res = [
            'total' => 0,
            'rows' => [],
        ];
        if (!empty($entityNameSearch)) {
            // todo 任务名称模糊搜索 暂时支持50条
            $entityIdList = model(SaasEntityModel::class)->where(['name' => $entityNameSearch, 'tenant_id' => $tenantId])->field('id')->limit(50)->column('id');
            if (empty($entityIdList)) {
                $res['total'] = 0;
                $res['rows'] = [];
                return $res;
            }
            $filter[] = ['entity_id' => ['IN', implode(',', $entityIdList)]];
        }
        $moduleIdList = $this->getBigModuleIdList($bigModuleCode);
        $queryFields = [
            'project_id',
            'entity_id',
            'task_id',
            'user_id',
            'step_category_name',
            'step_category_ids',
            'step_category_id',
            'step_id',
        ];
        $totalQuery = new WorkHourRecordModel();
        $totalQuery->where(['tenant_id' => $tenantId])->where($filter);
        $query = model(WorkHourRecordModel::class)
            ->where(['tenant_id' => $tenantId])
            ->where($filter)
            ->page(...$page);
        if (!empty($moduleIdList)) {
            $query->where(['module_id' => ['IN', $moduleIdList]]);
            $totalQuery->where(['module_id' => ['IN', $moduleIdList]]);
        }
        // 查询聚合数据 还是每天的详情
        if ($queryType === 'sum') {
            $query->group('entity_id,user_id,month_time');
            $res['total'] = $totalQuery->field('count(distinct entity_id,user_id,month_time) count')->limit(1)->value('count');
            $queryFields[] = 'sum(hour) hour';
            $queryFields[] = 'month_time';
        } else {
            $queryFields[] = 'hour';
            $queryFields[] = 'month_time';
            $queryFields[] = 'day_time';
            $res['total'] = $totalQuery->count();
        }
        $res['rows'] = $query->field($queryFields)->select();
        if (empty($res['rows'])) {
            return $res;
        }
        $taskIdList = [];
        $userIdList = [];
        $projectIdList = [];
        foreach ($res['rows'] as $row) {
            $taskIdList[$row['task_id']] = $row['task_id'];
            $userIdList[$row['user_id']] = $row['user_id'];
            $projectIdList[$row['project_id']] = $row['project_id'];
        }
        $taskList = model(SaasEntityModel::class)
            ->join('task on task.entity_id = entity.id')
            ->join('step_category on step_category.id = task.step_category_id')
            ->join('step on step.id = task.step_id')
            ->field(build_complex_query_fields(['task.id', 'task.name',
                'entity.id', 'entity.module_id', 'entity.initial_episode_code',
                'entity.showings_number', 'entity.name', 'entity.json',
                'step_category.id', 'step_category.name',
                'step.id', 'step.code', 'step.name',
            ]))
            ->where(['task.id' => ['IN', implode(',', $taskIdList)]])
            ->select();
        // 格式化任务数据
        $taskList = array_map(function ($row) {
            $row = separate_complex_query_data($row);
            $entityJson = json_decode($row['entity']['json'] ?? '{}', true);
            $entityModuleCode = SaasModuleService::getInstance()->byId($row['entity']['module_id'])['code'] ?? '';
            if (!empty($entityModuleCode)) {
                foreach ($entityJson as $ek => $ev) {
                    $ek = str_replace_once($entityModuleCode . '_', '', $ek);
                    $row['entity'][$ek] = $ev;
                }
            } else {
                foreach ($entityJson as $ek => $ev) {
                    $row['entity'][$ek] = $ev;
                }
            }
            unset($row['entity']['json']);
            $row['task_id'] = $row['task']['id'];
            return $row;
        }, $taskList);
        $taskList = array_column($taskList, null, 'task_id');
        // 查询project
        $projectList = model(SaasProjectModel::class)
            ->where(['id' => ['IN', implode(',', $projectIdList)]])
            ->field('id,name')
            ->select();
        $projectList = array_column($projectList, null, 'id');

        // 查询用户
        $userList = teamones_request('teamones-im', 'user/select', ['param' => [
            'filter' => [
                'id' => ['-in', implode(',', $userIdList)],
            ],
        ]])['data']['rows'];
        $userList = array_column($userList, null, 'id');

        // 组装数据
        foreach ($res['rows'] as &$row) {
            $row['user'] = $userList[$row['user_id']] ?? null;
            $row['project'] = $projectList[$row['project_id']] ?? null;
            if (isset($taskList[$row['task_id']])) {
                $row = array_merge($row, $taskList[$row['task_id']]);
            }
        }
        return $res;
    }

    /**
     * 获得大模块对应的module
     * @param string $bigModuleCode
     * @return array
     */
    private function getBigModuleIdList(string $bigModuleCode)
    {
        switch ($bigModuleCode) {
            case 'asset':
                $moduleIdList = [
                    SaasModuleService::getInstance()->byCode('design')['id'],
                    SaasModuleService::getInstance()->byCode('level')['id'],
                    SaasModuleService::getInstance()->byCode('asset')['id'],
                ];
                break;
            case 'shot':
                $moduleIdList = [
                    SaasModuleService::getInstance()->byCode('shot')['id'],
                ];
                break;
            default:
                $moduleIdList = [];
        }
        return $moduleIdList;
    }

    /**
     * 任务被返修的比例
     * @return float
     */
    private function getTaskRepairChancePercent()
    {
        $r = OptionsService::getInstance()->getOptionsConfigItemData('task_config', 'month_repair_percent', 0.18);
        return floatval($r);
    }

    private function getTaskRepairIncreaseHourPercent()
    {
        $r = OptionsService::getInstance()->getOptionsConfigItemData('task_config', 'repair_will_increase_percent', 0.2);
        return floatval($r);
    }

    /**
     * 计算当前任务可以跳过的天数
     * @param $userWorkEveryDayMin
     * @param $bestUserAvailableDaysIndexList
     * @param int $bestUserAvailableDaysIndexIndex
     * @param $userWorkHour
     * @return int
     * @throws Exception
     */
    private function calculateCanSkipDays($userWorkEveryDayMin, $bestUserAvailableDaysIndexList, int $bestUserAvailableDaysIndexIndex, $userWorkHour)
    {
        $skipDays = 0;
        $reverseSum = 0;
        $totalAvailableIndexCount = count($bestUserAvailableDaysIndexList);
        foreach (array_reverse($bestUserAvailableDaysIndexList) as $bestUserAvailableDaysIndexReversedIndex => $bestUserAvailableDaysIndexReversed) {
            $reverseIndex = $totalAvailableIndexCount - 1 - $bestUserAvailableDaysIndexReversedIndex;
            if ($reverseIndex <= $bestUserAvailableDaysIndexIndex) {
                // 如果顺序可用的工时里的index和倒序的index相等 说明没有可以跳过的天数 直接break;
                break;
            }
            $reverseDaysHour = $userWorkEveryDayMin[$bestUserAvailableDaysIndexReversed];
            $reverseSum += $reverseDaysHour;
            // 如果当前倒序合计工时 > 剩余的任务标用户标准工时 说明可以跳天
            if ($reverseSum < $userWorkHour) {
                continue;
            }
            // 计算可以跳过的天数
            $canSkipDays = $reverseIndex - $bestUserAvailableDaysIndexIndex;
            if ($canSkipDays > 1) {
                $skipDays = random_int(0, $canSkipDays);
            } else {
                $skipDays = 1;
            }
            break;
        }
        return $skipDays;
    }

    /**
     * 使用任务 来消费掉可以匹配的人员工时
     * @param array $task 任务信息
     * @param array $bestUserAvailableDaysIndexList 可用人员工时引用地址
     * @param float $userWorkHour 要消费的任务人员工时
     * @param float $workHourRatio 工时比例
     * @param float $userGradeRadio
     * @param array $planEpisodeManHour 剧集计划要消费的工时总和
     * @param array $workHourInsertList 待插入的工时记录
     * @param array $presetData 预设的工时记录数据
     * @return void
     */
    private function doConsumeUserWorkHour(array $task, array &$bestUserAvailableDaysIndexList,
        float &$userWorkHour, float $workHourRatio, float $userGradeRadio,
        array &$planEpisodeManHour, array &$workHourInsertList, array $presetData) {
        $passedDays = 0;
        foreach ($bestUserAvailableDaysIndexList as $dayTime => &$dayHours) {
            if ($userWorkHour == 0) {
                break;
            }
            if ($dayHours <= 0) {
                $passedDays += 1;
                continue;
            }

            $tempInsertData = $presetData;
            $tempInsertData['day_time'] = $dayTime;
            $tempInsertData['hour'] = min($dayHours, $userWorkHour) / $userGradeRadio;
            $workHourInsertList[] = $tempInsertData;

            APP_DEBUG && trace(date("Y-m-d", $dayTime) . ": cost user hour" . min($dayHours, $userWorkHour), 'DEBUG');
            if ($dayHours > $userWorkHour) {
                // 时间足够
                $dayHours -= $userWorkHour;
                $planEpisodeManHour[$task['episode_id']] -= $userWorkHour * $workHourRatio;
                $userWorkHour = 0;
                break;
            } else {
                // 今日时间不够
                $userWorkHour -= $dayHours;
                $planEpisodeManHour[$task['episode_id']] -= $dayHours * $workHourRatio;
                $dayHours = 0;
                $passedDays += 1;
            }
        }
        // 移除掉已经匹配完了的天 日工时为0的
        $bestUserAvailableDaysIndexList = array_slice($bestUserAvailableDaysIndexList, $passedDays,
            count($bestUserAvailableDaysIndexList) - $passedDays, true);
    }

    /**
     * 获得用户工时比例系数
     * @return array
     */
    private function getUserGradeWorkHourRadio()
    {
        $r = OptionsService::getInstance()->getOptionsConfigItemData('user_work_hour_config', 'grade_radio', []);
        return array_map("floatval", array_column($r, 'radio', 'grade'));
    }

    /**
     * 用户工时列表
     * @param int $tenantId
     * @param string $bigModuleCode
     * @param array $filter
     * @param array $page
     * @param $userNameSearch
     * @return array
     */
    public function listByUser(int $tenantId, string $bigModuleCode, array $filter, array $page, $userNameSearch)
    {
        $type = 'ding_hour';
        $res = [
            'total' => 0,
            'rows' => [],
        ];
        if (!empty($userNameSearch)) {
            // 查询用户
            $userList = teamones_request('teamones-im', 'user/select', rollback_parser_filter(['param' => [
                'filter' => [
                    'name' => $userNameSearch,
                ],
            ]]))['data']['rows'];
            $userIdList = array_column($userList, 'id');
            // todo 用户名称搜索 暂时支持50条 防止查询出来过多用户
            if (empty($userIdList)) {
                $res['total'] = 0;
                $res['rows'] = [];
                return $res;
            }
            $filter[] = ['user_id' => ['IN', implode(',', $userIdList)]];
        }
        $moduleIdList = $this->getBigModuleIdList($bigModuleCode);
        $queryFields = [
            'user_id',
            'month_time',
            'sum(hour) hour',
        ];
        $totalQuery = new WorkHourRecordModel();
        $totalQuery->where(['tenant_id' => $tenantId])->where($filter);
        $query = model(WorkHourRecordModel::class)
            ->where(['tenant_id' => $tenantId])
            ->where($filter)
            ->page(...$page);
        if (!empty($moduleIdList)) {
            $query->where(['module_id' => ['IN', $moduleIdList]]);
            $totalQuery->where(['module_id' => ['IN', $moduleIdList]]);
        }
        // 查询聚合数据 还是每天的详情
        $query->group('user_id,month_time');
        $res['total'] = $totalQuery->field('count(distinct user_id,month_time) count')->limit(1)->value('count');
        $res['rows'] = $query->field($queryFields)->select();
        if (empty($res['rows'])) {
            return $res;
        }
        $userIdList = [];
        foreach ($res['rows'] as $row) {
            $userIdList[$row['user_id']] = $row['user_id'];
        }
        // 查询用户
        $userList = teamones_request('teamones-im', 'user/select', ['param' => [
            'filter' => [
                'id' => ['-in', implode(',', $userIdList)],
            ],
        ]])['data']['rows'];
        $userList = array_column($userList, null, 'id');
        // 查询用户工时
        $monthStart = intval($filter['month_time']);
        $monthEnd = strtotime("+1 month -1 seconds", $monthStart);
        $queryParam = [
            'user_id_list' => $userIdList,
            'work_date_from' => date("Y-m-d 00:00:00", $monthStart),
            'work_date_to' => date("Y-m-d 00:00:00", $monthEnd),
        ];

        // if ($type === 'user_filled') {
        $userWorkHourList = teamones_request('teamones-olap-sr', 'work_time/get_user_work_times', $queryParam)['data'];
        // } else {
        // $userWorkHourList = teamones_request('teamones-work-hour', 'dingtalk/get_user_work_times', $queryParam)['data'];
        // }
        $userWorkHourList = array_column($userWorkHourList, 'work_hour', 'user_id');
        // 组装数据
        foreach ($res['rows'] as &$row) {
            $row['user'] = $userList[$row['user_id']] ?? null;
            $row['ding_work_hour'] = $userWorkHourList[$row['user_id']] ?? 0.00;
        }
        return $res;
    }

    /**
     * 用户月度日工时列表
     * @param int $tenantId
     * @param $filter
     * @return array
     */
    public function userDayHourList(int $tenantId, $filter)
    {
        $res = [];
        $monthStart = intval($filter['month_time']);
        $monthEnd = strtotime("+1 month -1 seconds", $monthStart);
        $queryParam = [
            'user_id_list' => [$filter['user_id']],
            'work_date_from' => date("Y-m-d 00:00:00", $monthStart),
            'work_date_to' => date("Y-m-d 00:00:00", $monthEnd),
        ];

        $userWorkHourList = teamones_request('teamones-work-hour', 'dingtalk/get_user_work_times', $queryParam)['data'];
        $userWorkHourList = array_shift($userWorkHourList);
        if (!empty($userWorkHourList)) {
            $userWorkHourList = $userWorkHourList['work_every_day_min'];
        }
        $userWorkHourList = array_map(function ($i) {
            return $i / 60;
        }, $userWorkHourList);
        $currentResTime = $monthStart;
        $resIndexMap = [];
        $resIndex = 0;
        while ($currentResTime <= $monthEnd) {
            $res[] = [
                'day_time' => $currentResTime,
                'ding_work_hour' => $userWorkHourList[$currentResTime] ?? 0.00,
                'task_work_hour' => 0.00,
            ];
            $resIndexMap[$currentResTime] = $resIndex;
            $resIndex += 1;
            $currentResTime += 60 * 60 * 24;
        }

        $queryFields = [
            'day_time',
            'sum(hour) hour',
        ];
        $filter['tenant_id'] = $tenantId;

        $dayHours = model(WorkHourRecordModel::class)
            ->where($filter)
            ->field($queryFields)
            ->group('day_time')
            ->select();
        if (empty($dayHours)) {
            return $res;
        }
        $dayHours = array_column($dayHours, 'hour', 'day_time');

        // 组装数据
        foreach ($dayHours as $dayTime => $hour) {
            if (isset($resIndexMap[$dayTime])) {
                $res[$resIndexMap[$dayTime]]['task_work_hour'] = $hour;
            }
        }
        return $res;
    }

    /**
     * 用户日任务工时列表
     * @param int $tenantId
     * @param $filter
     * @param array $page
     * @return array
     */
    public function userDayHourTaskList(int $tenantId, $filter, array $page)
    {
        $res = [
            'total' => 0,
            'rows' => [],
        ];
        $filter['tenant_id'] = $tenantId;
        if (!empty($entityNameSearch)) {
            // todo 任务名称模糊搜索 暂时支持50条
            $entityIdList = model(SaasEntityModel::class)->where(['name' => $entityNameSearch, 'tenant_id' => $tenantId])->field('id')->limit(50)->column('id');
            if (empty($entityIdList)) {
                $res['total'] = 0;
                $res['rows'] = [];
                return $res;
            }
            $filter[] = ['entity_id' => ['IN', implode(',', $entityIdList)]];
        }

        $queryFields = [
            'project_id',
            'task_id',
            'step_category_name',
            'step_category_ids',
            'step_category_id',
            'step_id',
            'sum(hour) hour',
        ];
        $totalQuery = new WorkHourRecordModel();
        $res['total'] = $totalQuery->where($filter)
            ->field('count(distinct task_id) count')
            ->limit(1)
            ->value('count');

        $res['rows'] = model(WorkHourRecordModel::class)
            ->where($filter)
            ->page(...$page)->field($queryFields)
            ->group('task_id')
            ->select();
        if (empty($res['rows'])) {
            return $res;
        }
        $taskIdList = [];

        $projectIdList = [];
        foreach ($res['rows'] as $row) {
            $taskIdList[$row['task_id']] = $row['task_id'];
            $projectIdList[$row['project_id']] = $row['project_id'];
        }
        $taskList = model(SaasEntityModel::class)
            ->join('task on task.entity_id = entity.id')
            ->join('step_category on step_category.id = task.step_category_id')
            ->join('step on step.id = task.step_id')
            ->field(build_complex_query_fields(['task.id', 'task.name',
                'entity.id', 'entity.module_id', 'entity.initial_episode_code',
                'entity.showings_number', 'entity.name', 'entity.json',
                'step_category.id', 'step_category.name',
                'step.id', 'step.code', 'step.name',
            ]))
            ->where(['task.id' => ['IN', implode(',', $taskIdList)]])
            ->select();
        // 格式化任务数据
        $taskList = array_map(function ($row) {
            $row = separate_complex_query_data($row);
            $entityJson = json_decode($row['entity']['json'] ?? '{}', true);
            $entityModuleCode = SaasModuleService::getInstance()->byId($row['entity']['module_id'])['code'] ?? '';
            if (!empty($entityModuleCode)) {
                foreach ($entityJson as $ek => $ev) {
                    $ek = str_replace_once($entityModuleCode . '_', '', $ek);
                    $row['entity'][$ek] = $ev;
                }
            } else {
                foreach ($entityJson as $ek => $ev) {
                    $row['entity'][$ek] = $ev;
                }
            }
            unset($row['entity']['json']);
            $row['task_id'] = $row['task']['id'];
            return $row;
        }, $taskList);
        $taskList = array_column($taskList, null, 'task_id');
        // 查询project
        $projectList = model(SaasProjectModel::class)
            ->where(['id' => ['IN', implode(',', $projectIdList)]])
            ->field('id,name')
            ->select();
        $projectList = array_column($projectList, null, 'id');
        // 组装数据
        foreach ($res['rows'] as &$row) {
            $row['project'] = $projectList[$row['project_id']] ?? null;
            if (isset($taskList[$row['task_id']])) {
                $row = array_merge($row, $taskList[$row['task_id']]);
            }
        }
        return $res;
    }

    /**
     * 获得最大划水小时百分比 整数 50 => 50%
     * @return int
     */
    private function getMaxPaddlePercent()
    {
        $r = OptionsService::getInstance()->getOptionsConfigItemData('user_work_hour_config', 'max_paddle_percent', 10);
        return intval($r);
    }
}
